/*
 * @(#)AuthorizationModule.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.IOException;
import java.net.ProtocolException;
import java.util.Hashtable;

/**
 * This module handles authentication requests. Authentication info is
 * preemptively sent if any suitable candidate info is available. If a request
 * returns with an appropriate status (401 or 407) then the necessary info is
 * sought from the AuthenticationInfo class.
 *
 * @author Ronald Tschal�r
 * @version 0.3-3 06/05/2001
 */
class AuthorizationModule implements HTTPClientModule {
    /** This holds the current Proxy-Authorization-Info for each HTTPConnection */
    private static Hashtable proxy_cntxt_list = new Hashtable();

    /**
     * a list of deferred authorization retries (used with
     * Response.retryRequest())
     */
    private static Hashtable deferred_auth_list = new Hashtable();

    /** counters for challenge and auth-info lists */
    private int auth_lst_idx, prxy_lst_idx, auth_scm_idx, prxy_scm_idx;

    /** the last auth info sent, if any */
    private AuthorizationInfo auth_sent;

    private AuthorizationInfo prxy_sent;

    /** is the info in auth_sent a preemtive guess or the result of a 4xx */
    private boolean auth_from_4xx;

    private boolean prxy_from_4xx;

    /** guard against bugs on both our side and the server side */
    private int num_tries;

    /** used for deferred authoriation retries */
    private Request saved_req;

    private Response saved_resp;

    private static final Logger log = Logger.getLogger(AuthorizationModule.class);

    // Constructors

    /** Initialize counters for challenge and auth-info lists. */
    AuthorizationModule() {
        auth_lst_idx = 0;
        prxy_lst_idx = 0;
        auth_scm_idx = 0;
        prxy_scm_idx = 0;

        auth_sent = null;
        prxy_sent = null;

        auth_from_4xx = false;
        prxy_from_4xx = false;

        num_tries = 0;
        saved_req = null;
        saved_resp = null;
    }

    // Methods

    /** Invoked by the HTTPClient. */
    public int requestHandler(Request req, Response[] resp) throws IOException, AuthSchemeNotImplException {
        HTTPConnection con = req.getConnection();
        AuthorizationHandler auth_handler = AuthorizationInfo.getAuthHandler();
        AuthorizationInfo guess;
        NVPair[] hdrs = req.getHeaders();
        int rem_idx = -1;

        // check for retries

        HttpOutputStream out = req.getStream();
        if (out != null && deferred_auth_list.get(out) != null) {
            copyFrom((AuthorizationModule)deferred_auth_list.remove(out));
            req.copyFrom(saved_req);

            if (log.isDebugEnabled())
                log.debug("Handling deferred auth challenge");

            handle_auth_challenge(req, saved_resp);

            if (auth_sent != null) {
                if (log.isDebugEnabled())
                    log.debug("Sending request with Authorization '" + auth_sent + "'");

            } else {

                if (log.isDebugEnabled())
                    log.debug("Sending request with Proxy-Authorization '" + prxy_sent + "'");
            }
            return REQ_RESTART;
        }

        // Preemptively send proxy authorization info

        Proxy:
        if (con.getProxyHost() != null && !prxy_from_4xx) {
            // first remove any Proxy-Auth header that still may be around

            for (int idx = 0; idx < hdrs.length; idx++) {
                if (hdrs[idx].getName().equalsIgnoreCase("Proxy-Authorization")) {
                    rem_idx = idx;
                    break;
                }
            }
            Hashtable proxy_auth_list = Util.getList(proxy_cntxt_list, con.getContext());
            guess = (AuthorizationInfo)proxy_auth_list.get(con.getProxyHost() + ":" + con.getProxyPort());
            if (guess == null)
                break Proxy;

            if (auth_handler != null) {
                try {
                    guess = auth_handler.fixupAuthInfo(guess, req, null, null);
                } catch (AuthSchemeNotImplException asnie) {
                    break Proxy;
                }
                if (guess == null)
                    break Proxy;
            }

            if (rem_idx == -1) // add proxy-auth header
            {
                rem_idx = hdrs.length;
                hdrs = Util.resizeArray(hdrs, rem_idx + 1);
                req.setHeaders(hdrs);
            }

            hdrs[rem_idx] = new NVPair("Proxy-Authorization", guess.toString());
            rem_idx = -1;

            prxy_sent = guess;
            prxy_from_4xx = false;

            if (log.isDebugEnabled())
                log.debug("Preemptively sending Proxy-Authorization '" + guess + "'");

        }
        if (rem_idx >= 0) {
            System.arraycopy(hdrs, rem_idx + 1, hdrs, rem_idx, hdrs.length - rem_idx - 1);
            hdrs = Util.resizeArray(hdrs, hdrs.length - 1);
            req.setHeaders(hdrs);
        }

        // Preemptively send authorization info

        rem_idx = -1;
        Auth:
        if (!auth_from_4xx) {
            // first remove any Auth header that still may be around

            for (int idx = 0; idx < hdrs.length; idx++) {
                if (hdrs[idx].getName().equalsIgnoreCase("Authorization")) {
                    rem_idx = idx;
                    break;
                }
            }

            // now try and guess whether we need to send auth info

            guess = AuthorizationInfo.findBest(req);
            if (guess == null)
                break Auth;

            if (auth_handler != null) {
                try {
                    guess = auth_handler.fixupAuthInfo(guess, req, null, null);
                } catch (AuthSchemeNotImplException asnie) {
                    break Auth;
                }
                if (guess == null)
                    break Auth;
            }

            if (rem_idx == -1) // add auth header
            {
                rem_idx = hdrs.length;
                hdrs = Util.resizeArray(hdrs, rem_idx + 1);
                req.setHeaders(hdrs);
            }

            hdrs[rem_idx] = new NVPair("Authorization", guess.toString());
            rem_idx = -1;

            auth_sent = guess;
            auth_from_4xx = false;

            if (log.isDebugEnabled())
                log.debug("Preemptively sending Authorization '" + guess + "'");

        }
        if (rem_idx >= 0) {
            System.arraycopy(hdrs, rem_idx + 1, hdrs, rem_idx, hdrs.length - rem_idx - 1);
            hdrs = Util.resizeArray(hdrs, hdrs.length - 1);
            req.setHeaders(hdrs);
        }

        return REQ_CONTINUE;
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase1Handler(Response resp, RoRequest req) throws IOException {
      /*
       * If auth info successful update path list. Note: if we preemptively sent
       * auth info we don't actually know if it was necessary. Therefore we don't
       * update the path list in this case; this prevents it from being
       * contaminated. If the info was necessary, then the next time we access
       * this resource we will again guess the same info and send it.
       */
        if (resp.getStatusCode() != 401 && resp.getStatusCode() != 407) {
            if (auth_sent != null && auth_from_4xx) {
                try {
                    AuthorizationInfo.getAuthorization(auth_sent, req, resp, false).addPath(req.getRequestURI());
                } catch (AuthSchemeNotImplException asnie) { /* shouldn't happen */
                }
            }

            // reset guard if not an auth challenge
            num_tries = 0;
        }

        auth_from_4xx = false;
        prxy_from_4xx = false;

        if (resp.getHeader("WWW-Authenticate") == null) {
            auth_lst_idx = 0;
            auth_scm_idx = 0;
        }

        if (resp.getHeader("Proxy-Authenticate") == null) {
            prxy_lst_idx = 0;
            prxy_scm_idx = 0;
        }
    }

    /** Invoked by the HTTPClient. */
    public int responsePhase2Handler(Response resp, Request req) throws IOException, AuthSchemeNotImplException {
        // Let the AuthHandler handle any Authentication headers.

        AuthorizationHandler h = AuthorizationInfo.getAuthHandler();
        if (h != null)
            h.handleAuthHeaders(resp, req, auth_sent, prxy_sent);

        // handle 401 and 407 response codes

        int sts = resp.getStatusCode();
        switch (sts) {
            case 401: // Unauthorized
            case 407: // Proxy Authentication Required

                // guard against infinite retries due to bugs

                num_tries++;
                if (num_tries > 10)
                    throw new ProtocolException("Bug in authorization handling: server refused the given info 10 times");

                // defer handling if a stream was used

                if (req.getStream() != null) {
                    if (!HTTPConnection.deferStreamed) {
                        if (log.isDebugEnabled())
                            log.debug("Status " + sts + " not handled - request has an output stream");

                        return RSP_CONTINUE;
                    }

                    saved_req = (Request)req.clone();
                    saved_resp = (Response)resp.clone();
                    deferred_auth_list.put(req.getStream(), this);

                    req.getStream().reset();
                    resp.setRetryRequest(true);

                    if (log.isDebugEnabled())
                        log.debug("Handling of status " + sts + " deferred because an output stream was used");

                    return RSP_CONTINUE;
                }

                // handle the challenge

                if (log.isDebugEnabled())
                    log.debug("Handling status: " + sts + " " + resp.getReasonLine());

                handle_auth_challenge(req, resp);

                // check for valid challenge

                if (auth_sent != null || prxy_sent != null) {
                    try {
                        resp.getInputStream().close();
                    } catch (IOException ioe) {
                    }

                    if (auth_sent != null) {
                        if (log.isDebugEnabled())
                            log.debug("Resending request with Authorization '" + auth_sent + "'");
                    } else {
                        if (log.isDebugEnabled())
                            log.debug("Resending request with Proxy-Authorization '" + prxy_sent + "'");
                    }

                    return RSP_REQUEST;
                }

                if (req.getStream() != null) {
                    if (log.isDebugEnabled())
                        log.debug("Status " + sts + " not handled - request has an output stream");
                } else {
                    if (log.isDebugEnabled())
                        log.debug("No Auth Info found - status " + sts + " not handled");
                }
                return RSP_CONTINUE;

            default:

                return RSP_CONTINUE;
        }
    }

    /** Invoked by the HTTPClient. */
    public void responsePhase3Handler(Response resp, RoRequest req) {
    }

    /** Invoked by the HTTPClient. */
    public void trailerHandler(Response resp, RoRequest req) throws IOException {
        // Let the AuthHandler handle any Authentication headers.

        AuthorizationHandler h = AuthorizationInfo.getAuthHandler();
        if (h != null)
            h.handleAuthTrailers(resp, req, auth_sent, prxy_sent);
    }

    /**
     *
     */
    private void handle_auth_challenge(Request req, Response resp) throws AuthSchemeNotImplException, IOException {
        // handle WWW-Authenticate

        int[] idx_arr = {auth_lst_idx, // hack to pass by ref
                         auth_scm_idx};
        auth_sent = setAuthHeaders(resp.getHeader("WWW-Authenticate"), req, resp, "Authorization", idx_arr, auth_sent);
        if (auth_sent != null) {
            auth_from_4xx = true;
            auth_lst_idx = idx_arr[0];
            auth_scm_idx = idx_arr[1];
        } else {
            auth_lst_idx = 0;
            auth_scm_idx = 0;
        }

        // handle Proxy-Authenticate

        idx_arr[0] = prxy_lst_idx; // hack to pass by ref
        idx_arr[1] = prxy_scm_idx;
        prxy_sent =
                setAuthHeaders(resp.getHeader("Proxy-Authenticate"), req, resp, "Proxy-Authorization", idx_arr, prxy_sent);
        if (prxy_sent != null) {
            prxy_from_4xx = true;
            prxy_lst_idx = idx_arr[0];
            prxy_scm_idx = idx_arr[1];
        } else {
            prxy_lst_idx = 0;
            prxy_scm_idx = 0;
        }

        if (prxy_sent != null) {
            HTTPConnection con = req.getConnection();
            Util.getList(proxy_cntxt_list, con.getContext()).put(con.getProxyHost() + ":" + con.getProxyPort(), prxy_sent);
        }

        // check for headers

        if (auth_sent == null && prxy_sent == null && resp.getHeader("WWW-Authenticate") == null
            && resp.getHeader("Proxy-Authenticate") == null) {
            if (resp.getStatusCode() == 401)
                throw new ProtocolException("Missing WWW-Authenticate header");
            else
                throw new ProtocolException("Missing Proxy-Authenticate header");
        }
    }

    /**
     * Handles authentication requests and sets the authorization headers. It
     * tries to retrieve the neccessary parameters from AuthorizationInfo, and
     * failing that calls the AuthHandler. Handles multiple authentication
     * headers.
     *
     * @param auth_str
     *         the authentication header field returned by the server.
     * @param req
     *         the Request used
     * @param resp
     *         the full Response received
     * @param header
     *         the header name to use in the new headers array.
     * @param idx_arr
     *         an array of indicies holding the state of where we are when
     *         handling multiple authorization headers.
     * @param prev
     *         the previous auth info sent, or null if none
     * @return the new credentials, or null if none found
     * @throws ProtocolException
     *         if <var>auth_str</var> is null.
     * @throws AuthSchemeNotImplException
     *         if thrown by the AuthHandler.
     * @throws IOException
     *         if thrown by the AuthHandler.
     */
    private AuthorizationInfo setAuthHeaders(String auth_str, Request req, RoResponse resp, String header,
                                             int[] idx_arr, AuthorizationInfo prev)
            throws ProtocolException, AuthSchemeNotImplException, IOException {
        if (auth_str == null)
            return null;

        // get the list of challenges the server sent
        AuthorizationInfo[] challenges = AuthorizationInfo.parseAuthString(auth_str, req, resp);

        if (log.isDebugEnabled()) {
            log.debug("Parsed " + challenges.length + " challenges:");
            for (int idx = 0; idx < challenges.length; idx++)
                log.debug("AuthM: Challenge " + challenges[idx]);
        }

        if (challenges.length == 0)
            return null;

      /*
       * some servers expect a 401 to invalidate sent credentials. However, only
       * do this for Basic scheme (because e.g. digest "stale" handling will fail
       * otherwise)
       */
        if (prev != null && prev.getScheme().equalsIgnoreCase("Basic")) {
            for (int idx = 0; idx < challenges.length; idx++)
                if (prev.getRealm().equals(challenges[idx].getRealm())
                    && prev.getScheme().equalsIgnoreCase(challenges[idx].getScheme()))
                    AuthorizationInfo.removeAuthorization(prev, req.getConnection().getContext());
        }

        AuthorizationInfo credentials = null;
        AuthorizationHandler auth_handler = AuthorizationInfo.getAuthHandler();

        // try next auth challenge in list
        while (credentials == null && idx_arr[0] != -1 && idx_arr[0] < challenges.length) {
            credentials = AuthorizationInfo.getAuthorization(challenges[idx_arr[0]], req, resp, false);
            if (auth_handler != null && credentials != null)
                credentials = auth_handler.fixupAuthInfo(credentials, req, challenges[idx_arr[0]], resp);
            if (++idx_arr[0] == challenges.length)
                idx_arr[0] = -1;
        }

        // if we don't have any credentials then prompt the user
        if (credentials == null) {
            for (int idx = 0; idx < challenges.length; idx++) {
                if (idx_arr[1] >= challenges.length)
                    idx_arr[1] = 0;

                try {
                    credentials = AuthorizationInfo.queryAuthHandler(challenges[idx_arr[1]], req, resp);
                    break;
                } catch (AuthSchemeNotImplException asnie) {
                    if (idx == challenges.length - 1)
                        throw asnie;
                } finally {
                    idx_arr[1]++;
                }
            }
        }

        // if we still don't have any credentials then give up
        if (credentials == null)
            return null;

        // find auth info
        int auth_idx;
        NVPair[] hdrs = req.getHeaders();
        for (auth_idx = 0; auth_idx < hdrs.length; auth_idx++) {
            if (hdrs[auth_idx].getName().equalsIgnoreCase(header))
                break;
        }

        // add credentials to headers
        if (auth_idx == hdrs.length) {
            hdrs = Util.resizeArray(hdrs, auth_idx + 1);
            req.setHeaders(hdrs);
        }
        hdrs[auth_idx] = new NVPair(header, credentials.toString());

        return credentials;
    }

    private void copyFrom(AuthorizationModule other) {
        this.auth_lst_idx = other.auth_lst_idx;
        this.prxy_lst_idx = other.prxy_lst_idx;
        this.auth_scm_idx = other.auth_scm_idx;
        this.prxy_scm_idx = other.prxy_scm_idx;

        this.auth_sent = other.auth_sent;
        this.prxy_sent = other.prxy_sent;

        this.auth_from_4xx = other.auth_from_4xx;
        this.prxy_from_4xx = other.prxy_from_4xx;

        this.num_tries = other.num_tries;

        this.saved_req = other.saved_req;
        this.saved_resp = other.saved_resp;
    }
}
